今日的文章,將簡單的看過電腦的啟動流程,以及在 xv6 中的 bootloader 和 kernel.ld
這個檔案。
當電腦開機或是重新啟動時,我們需要一個初始化程式執行,這個初始化程式有時候會稱為 bootstrap program。這支程式通常會存放在 Read-Only-Memory (ROM) 或是 Electrically Erasable Programmable Read-Only-Memory (EEPROM)。
bootstrap program, 是一隻被電腦執行的程式,而對於硬體來說他十分接近硬體層面並且操控了許多重要的部分,角色上類似於韌體 (firmware)。bootstrap program 會初始化許多電腦硬體,包含 CPU 暫存器,裝置的控制器,或是記憶體的內容等等。並且需要將作業系統從硬碟載入到記憶體中等待執行。接著作業系統便會開始執行,並執行第一個 process,接著等待一些事件 (event) 發生,這一部分將會在下一篇提及。本篇我們聚焦在 xv6 中 linker file 的部分。
事件 (event) :
事件為一個被軟體所偵測到的動作,而事件又可以區分成同步 (synchronously) 與異步 (asynchronously)
在 RISC-V xv6 中,並不關心 bootloader 的部分,這一點和 x86 版本的 xv6 不同,詳細可以參考此 github 連接,可以看到曾經有 bootloader 的部分,但是在某一次 commit 後移除了。
kernel.ld
kernel.ld
不是 C 語言程式碼,也不是組合語言,他屬於 linker script file,用於告知 linker 該如何組織 object file 的資料,特定的資料需要放置在記憶體中哪一個區域等等。
linker 的功用為將多個 object file 合在一起形成一個可執行檔 (object file 中會有不同的 section 進行標記,如 text, code, bss 等等),在 linux 中可執行檔為 ELF (Executable and Linking Format) 格式 (object file 也使用此格式),linker 還會組織這一些 object file,讓這一些資料放置於記憶體中特定的區塊,在可執行檔執行時,就會按照 linker script file 所描述的方式放置資料到記憶體中。
object file vs executable file :
- object file : 由 compiler 產生,不可直接執行
- executable file : 由 linker 產生,可以直接執行,多個
.obj
檔組成.out
檔。object file 和 executable file 的格式為 ELF 格式 Executable and Linking Format。
ELF 格式,從名稱中可以判斷出定義了 object file 裡面應該放什麼類型的資料,以及要使用怎樣的格式去放置這一些資料。ELF 格式的檔案大致上有以下三種
.o
檔.out
檔.so
檔從上面這個 ELF 架構中,我們可以看到很多資料所組成的 section。
在之後當我們執行 kernel 時,QEMU 會執行可執行檔並按照 linker 所描述放置資料。
在常規情況下,可執行檔執行時,指令 (機器碼) 會放到 text section 預設的地方,以初始化變數放到 data section 預設的地方,未初始化變數會放置到 code section 預設的地方。
但假設我們想要自定義可執行檔執行後,這一些指令,初始化變數與未初始化變數放置的具體記憶體地址以及 section,我們就需要使用 linker script file 了。假設想要把 code
放到0x10000000
,data
放到 0x80000000
,則 linker script file 應該為以下。
OUTPUT_ARCH( "riscv" )
SECTIONS
{
. = 0x10000;
.text :
{
*(.text)
}
. = 0x8000000;
.data :
{
*(.data)
}
.bss :
{
*(.bss)
}
}
< 3.3 Simple Linker Script Example >
而以下為 xv6 中的 linker script file,位於kernel/kernel.ld
OUTPUT_ARCH( "riscv" )
ENTRY( _entry )
SECTIONS
{
/*
* ensure that entry.S / _entry is at 0x80000000,
* where qemu's -kernel jumps.
*/
. = 0x80000000;
.text : {
*(.text .text.*)
. = ALIGN(0x1000);
_trampoline = .;
*(trampsec)
. = ALIGN(0x1000);
ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");
PROVIDE(etext = .);
}
.rodata : {
. = ALIGN(16);
*(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
. = ALIGN(16);
*(.rodata .rodata.*)
}
.data : {
. = ALIGN(16);
*(.sdata .sdata.*) /* do not need to distinguish this from .data */
. = ALIGN(16);
*(.data .data.*)
}
.bss : {
. = ALIGN(16);
*(.sbss .sbss.*) /* do not need to distinguish this from .bss */
. = ALIGN(16);
*(.bss .bss.*)
}
PROVIDE(end = .);
}
在最一開始會告知 linker 輸出的可執行檔為 RISC-V 的指令集架構,接著下面會設置程式進入點,進入點定義在entry.S
中。程式進入點的意義為程式中第一條指令開始執行的位置,可以使用一個標籤進行標記,這裡使用_entry
這個標籤進行標記。
接著 linker script file 中可以看到 4 個 section,分別為 text, rodata, data, bss。我們在 0x80000000
前面看到一個.
,表示存取目前的記憶體地址 (意義上就是 location counter),而. = 0x80000000
表示從0x80000000
這個記憶體地址開始 (將 location counter 移動到這個位置)。
.text : {
*(.text .text.*)
. = ALIGN(0x1000);
_trampoline = .;
*(trampsec)
. = ALIGN(0x1000);
ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");
PROVIDE(etext = .);
}
最外層的.text
表示 text segment,裡面的*(.text .text.*)
表示蒐集 link 進來的所有 object file 的 text section (也就是要放入 text segment 的內容),並將這一些 text section 組織成一個.text
的 segment。
而下面的ALIGN
表示以 4096 bytes 的單位進行對齊 (同時這也是 xv6 中定義在記憶體中,一個分頁的大小 (分頁為記憶體區塊的單位))。在這麼分頁之後,代表的意義為 trapoline page,標籤為 trampsec (用於處理 trap,後面會說明 trap),並同樣以 4096 bytes 進行對齊。
目前記憶體地址應該位於 trapoline page 的頂端邊界,如果和 trapoline page 最低的記憶體地址相減並非為一個記憶體分頁的大小,則ASSERT
會發出錯誤 (在 C 語言中也有類似的機制,位於assert.h
中,可用於除錯)。接著將目前的記憶體地址以_etext
標籤標記。
而後面的 section 也會以相似的方式進行。整個記憶體的組織就會變成下圖所示,當可執行檔執行時,資料就會按照下面方式進行擺放,text 的資料就放到 text section 中,data 的資料就放到 data section 中。
明天,我們將要結合今天的內容與先前介紹的 RISC-V 基本操作以及一些特權模式等等,來探討與追蹤 xv6 的啟動與架構
from Source to Binary: How GNU Toolchain Works
readelf elf文件格式分析
xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book